/*********************************************************************
 *                                                                   *
 *  Make Directory Difference ( MakeDirDiff )                        *
 *  Author: Igor Kanshyn                                             *
 *  Copyright (c) 2009, Igor Kanshyn. All rights reserved.           *
 *                                                                   *
 *  MakeDirDiff is released under the Apache Software License.       *
 *  See LICENSE.txt for more details.                                *
 *                                                                   *
 *********************************************************************/
package org.grenader.makedirdiff;

import org.grenader.jscssmin.files.FileUtils;
import org.apache.tools.ant.taskdefs.MatchingTask;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;

import java.io.File;
import java.io.IOException;
import java.util.*;

/**
 * <p>Product: MakeDirDiff ( Make Directory Difference) <br> Author: Igor Kanshyn
 * (grenader). </p> Date: Sep 11, 2009
 */
public class MakeDirDiff extends MatchingTask {
  private File prevDir;
  private File destDir;

  private boolean processPreviousDir = true;
  private boolean failOnError = false;
  private List<File> newFiles = new ArrayList<File>();
  private int processedFilesCount = 0;
  private boolean cleanDestDir;

  private List<FileSet> filesets = new ArrayList<FileSet>();

  /**
   * <p>Brief help text.<br>
   * It will be shown in response on wrong set of input command line parameters.</p>
   */
  static final String HELP_TEXT = "Example: java -cp *; org.grenader.makedirdiff.MakeDirDiff c:/tmp/original c:/tmp/result c:/tmp/previous\n\n" +
      "MakeDirDiff requires three parameters and optional flags\n" +
      "  Parameters:\n"+
      "    <original data directory>\n"+
      "    <result directory>\n"+
      "    <previous data directory>\n"+
      "  Optional flags. They can be located in any order in command line and mixed with parameters.\n"+
      "    -cleanDestDir. Since ver 1.4. This flag makes the application to clean <result directory> at the beginning of work.\n";

    public void addFileset(FileSet set) {
    filesets.add(set);
  }

  /**
   * <p>Recursive file listing under a specified directory. <p/>
   * <p>Parameters<br><br>
   * MakeDirDiff requires at least three command line paraments.<br>
   * Example: java -cp *; org.grenader.makedirdiff.MakeDirDiff c:/tmp/original c:/tmp/result c:/tmp/previous<br><br>
   *
   * MakeDirDiff requires three parameters and optional flags:<br>
   * &nbsp;&nbsp;Parameters:<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;&lt;original data directory><br>
   * &nbsp;&nbsp;&nbsp;&nbsp;&lt;result directory><br>
   * &nbsp;&nbsp;&nbsp;&nbsp;&lt;previous data directory><br>
   *
   * &nbsp;&nbsp;Optional flags. They can be located in any order in command line and mixed with parameters.<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;&lt;-cleanDestDir. Since ver 1.4 This flag makes the application to clean <result directory> at the beginning of work.<br>
   * </p>
   *
   *
   * @param args <tt>args[0]</tt> can be optional flags. <br>
   * args <tt>args[1]</tt> is the full name of original (source) directory.<br>
   * The directory should existing directory and being readable.<br>
   * <tt>args[2]</tt> is the full name of result (destination) directory.<br>
   * The directory will be created if required.<br>
   * <tt>args[3]</tt> is the full name of previous data directory.<br>
   * The directory will be created if required.<br>
   *
   * @throws IOException in case some I/O errors
   */
  public static void main(String... args) throws IOException {
    MakeDirDiff diff = new MakeDirDiff();

      List<String> flags = new ArrayList<String>();
      List<String> fileNames = new ArrayList<String>();
      for (String arg : args) {
          if (arg.startsWith("-"))
              flags.add(arg);
          else
              fileNames.add(arg);

      }

      if (fileNames.size() < 3) {
      System.err.print("MakeDirDiff requires at least three command line paraments.\n");
      System.err.print(HELP_TEXT);
      System.err.print("\n");
      return;
    }

    diff.setProject(new Project());
    diff.setSrcDir(new File(fileNames.get(0)));
    diff.setDestDir(new File(fileNames.get(1)));
    diff.setPrevDir(new File(fileNames.get(2)));

    // Analyze flags
    if (flags.size() > 0) {
        if (flags.contains("-cleanDestDir"))
          diff.setCleanDestDir(true);
        if (flags.contains("-doNotCopyToPreviousDir"))
          diff.setProcessPreviousDir(false); //todo: we need test for that
    }
      diff.execute();
  }

  /**
   * <p>Ant task interface implementation.</p>
   * <p>Executes the task, gets paramaters from the class.</p>
   */
  public void execute() {
    try {
      makeDiff();
    }
    catch (IOException e) {
      System.err.println("Error: " + e.getMessage());
      e.printStackTrace();
    }

    showResultMessage();

/*
        Collections.sort(allFiles);

        for (File file : allFiles) {
            System.out.println("file = " + file);
        }

        Collections.sort(newFiles);
*/
  }

  /**
   * <p>Main task method.</p>
   * <p>Checks parameters first, create some directories, if they doesn't exists, scan and process filesets.</p>
   *
   * @throws IOException in case there are problem with reading/creating/copying files or directories
   */
  void makeDiff() throws IOException {
    checkParameters();
    prepare();

    newFiles = new ArrayList<File>();
    processedFilesCount = 0;

    scanAndProcess();
  }

  private void showResultMessage() {
    System.out.println(processedFilesCount + " file(s) have been processed.");
    System.out.println(newFiles.size() + " new/updated file(s) have been found.");
  }

  private void scanAndProcess() throws IOException {
    for (FileSet fs : filesets) {
      try {
        DirectoryScanner ds = fs.getDirectoryScanner(getProject());
        String[] files = ds.getIncludedFiles();
        File d = fs.getDir(getProject());

        if (files.length > 0) {
          System.out.println("FileSet dir: "+fs.getDir()+". Processing "+files.length+" files ...");
          for (String file : files) {
            processFile(d, new File(d, file));
          }
        }
      }
      catch (BuildException be) {
        // directory doesn't exist or is not readable
        throw be;
      }
      catch (Exception e) {
        e.printStackTrace();
        throw new BuildException(e);
      }
    }
  }

  private void processFile(File currentDir, File file) throws IOException {
    // We assume that we have only files (no directories) here

    processedFilesCount++;
    File oldFile = getPreviousDataFile(currentDir, file);
    if (!isFileTheSame(file, oldFile)) {
      File newFile = getResultFile(currentDir, file);
      if (processPreviousDir) {
        FileUtils.copyFileWithOriginalLastModifiedDate(file, oldFile);
      }
      FileUtils.copyFileWithOriginalLastModifiedDate(file, newFile);
      newFiles.add(file);
    }
  }

  private File getResultFile(File currentDir, File file) {
    String internalFilePath =
        file.getAbsolutePath().substring(currentDir.getAbsolutePath().length());
    return new File(destDir.getAbsolutePath(), internalFilePath);
  }

  private File getPreviousDataFile(File currentDir, File file) {
    String internalFilePath =
        file.getAbsolutePath().substring(currentDir.getAbsolutePath().length());
    return new File(prevDir.getAbsolutePath(), internalFilePath);
  }

    /**
     * <p>Check two different files and answer whether they are different or not. <p/>
     * <p>Note, it doesn't check file content. Only attrinutes for now. <p/>
     * @param oldFile - file that is suppost to be an old version
     * @param file - file that is suppost to be a new version
     * @return - true if files is the same one, false if they are different
     */
    private boolean isFileTheSame(File oldFile, File file) {
    if (!oldFile.exists() || !file.exists()) return false;
    if (!file.isDirectory() && oldFile.length() != file.length()) return false;

    return oldFile.lastModified() == file.lastModified();
  }

  /**
   * <p>Directory is valid if it exists, does not represent a file, and can be read.</p>
   * @param dirType directory aliase
   * @param aDirectory directory system file
   * @param createIfNo true - create a dirrectory if this doesn't exists, false - do not create
   *
   * @throws IOException in case something wrong with one of these operations
   */
  void validateDirectory(String dirType, File aDirectory, boolean createIfNo) throws IOException {
    if (aDirectory == null) {
      throw new IllegalArgumentException(dirType + " directory should not be null.");
    }
    if (!aDirectory.exists() && createIfNo) {
      System.err.println(dirType + " directory does not exist: " + aDirectory);
      if (aDirectory.mkdirs()) {
        System.err.println(dirType + " directory " + aDirectory + " has been created.");
      }
      else {
        throw new IOException(dirType + " directory " + aDirectory + " has NOT been created.");
      }

    }

    if (!aDirectory.exists()) {
      throw new IllegalArgumentException(dirType + " directory does not exist: " + aDirectory);
    }
    if (!aDirectory.isDirectory()) {
      throw new IllegalArgumentException(dirType + " directory is not a directory: " + aDirectory);
    }
    if (!aDirectory.canRead()) {
      throw new IllegalArgumentException(dirType + " directory cannot be read: " + aDirectory);
    }
  }

  /**
   * <p>Check whether all required attributes have been set and nothing silly has been entered.</p>
   *
   * @throws BuildException if an error occurs
   * @since Ant 1.5
   */
  protected void checkParameters() throws BuildException {
    if (filesets == null || filesets.size() == 0) {
      if (failOnError) throw new BuildException("No fileset was provided!");
      System.err.println("No fileset was provided, doing nothing");
      return;
    }

    try {
      for (FileSet fileSet : filesets) {
        validateDirectory("Original", fileSet.getDir(), false);
      }
      validateDirectory("Result", destDir, true);
      validateDirectory("Previous data", prevDir, true);
    }
    catch (IOException e) {
      e.printStackTrace();
      throw new BuildException(e.getMessage());
    }

  }

  /**
   * <p>Make some preparation before running.</p>
   * <p>For now it only:
   * <ul>
   * <li>clears result directory if it's defined by cleanDestDir param.</li>
   * </ul></p>
   *
   */
  protected void prepare(){
      if (cleanDestDir)
      {
          FileUtils.deleteDirectory(this.destDir);
      }
  }

  public File getPrevDir() {
    return prevDir;
  }

  public void setPrevDir(File prevDir) {
    this.prevDir = prevDir;
  }

  private void setSrcDir(File srcDir) {
    FileSet fileSet = new FileSet();
    fileSet.setDir(srcDir);
    this.addFileset(fileSet);
  }

  public void setDestDir(File destDir) {
    this.destDir = destDir;
  }

  /**
   * <p>Gets property that allows the task to store processed files in previous dir</p>
   * <p>Turning off (false) this property will make the task simply copy all filesets into result dir.</p>
   *
   * @return true is previuosDir is used, false otherwise
   */
  public boolean isProcessPreviousDir() {
    return processPreviousDir;
  }

  /**
   * <p>Sets property that allows the task to store processed files in previous dir</p>
   * <p>Turning off (false) this property will make the task simply copy all filesets into result dir.</p>

   * @param processPreviousDir new value
   */
  public void setProcessPreviousDir(boolean processPreviousDir) {
    this.processPreviousDir = processPreviousDir;
  }

  /**
   * <p>Stores all new new/updated file names.</p>
   * <p>This is just for test purposes.</p>
   * @return list of Files
   */
  public List<File> getNewFiles() {
    return newFiles;
  }

  public int getProcessedFilesCount() {
    return processedFilesCount;
  }

  public void setFailOnError(boolean failOnError) {
    this.failOnError = failOnError;
  }

    public boolean isCleanDestDir() {
        return cleanDestDir;
    }

    public void setCleanDestDir(boolean cleanDestDir) {
        this.cleanDestDir = cleanDestDir;
    }
}


